package edu.njit.hz278.core.web;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Properties;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import edu.njit.hz278.core.db.Query;

/**
 * Global listener. For now, it takes care of creating associations for a Java
 * method and the requested URL.
 * 
 * @author Rivers Zhang
 */
@WebListener
public class Listener implements ServletContextListener {

	private static void initHandlers(String initPkgs) {

		if (StringUtils.isEmpty(initPkgs))
			throw new RuntimeException("Packages are not defined.");

		String[] pkgs = StringUtils.split(initPkgs, ',');

		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		if (loader == null)
			loader = ClassLoader.getSystemClassLoader();

		/**
		 * for each package, we first get all class files inside this package.
		 * and then, get appname and modulename metas from file path. finally
		 * invoke __initHandler() to create handlers.
		 */
		for (String pkg : pkgs) {
			String path = StringUtils.replace(pkg, ".", "/");
			try {
				Enumeration<URL> resources = loader.getResources(path);
				while (resources.hasMoreElements()) {
					Collection<File> files = FileUtils.listFiles(new File(
							resources.nextElement().getFile()),
							new String[] { "class" }, true);

					for (File file : files) {
						String[] parts = StringUtils.split(
								file.getAbsolutePath(), File.separator);

						/**
						 * I won't check if the path is valid. Passing in valid
						 * parameters is developers' own responsibility. Here I
						 * simply assume the last element is module name and the
						 * second to the last is app name.
						 */

						int len = parts.length;
						String appName = parts[len - 2], moduleName = parts[len - 1];

						__initHandlers(pkg, appName, moduleName.substring(0,
								moduleName.length() - 6));

					}
				}
			} catch (IOException e) {
				log.error("Cannot resolve resources:" + pkg, e);
			}
		}

	}

	private static String normalize(String s) {
		char[] a = s.toCharArray();
		StringBuilder sb = new StringBuilder();

		sb.append(a[0] >= 'A' && a[0] <= 'Z' ? (char) (a[0] - 'A' + 'a') : a[0]);

		for (int i = 1, len = a.length; i < len; ++i) {
			char ch = a[i];
			if (ch >= 'A' && ch <= 'Z')
				sb.append('_').append((char) (ch - ('A' - 'a')));
			else
				sb.append(ch);
		}

		return sb.toString();
	}

	/**
	 * Initialize handlers given @appName and @moduleName.
	 * 
	 * @param pkgPrefix
	 * @param appName
	 * @param moduleName
	 * @param handlers
	 */
	private static void __initHandlers(String pkgPrefix, String appName,
			String moduleName) {

		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		if (loader == null)
			loader = ClassLoader.getSystemClassLoader();

		/**
		 * init new class based on the given class (composed of package name,
		 * app name and module name).
		 */

		try {
			Class<?> cls = loader.loadClass(pkgPrefix + "." + appName + "."
					+ moduleName);
			Object module = cls.newInstance();

			/**
			 * for every method inside this class, we do: [1] check if the
			 * method is declared as public and is associated with any one of
			 * Get, Post, Put and Delete annotations. if so, go to step 2.
			 * otherwise continue the loop. [2] create a handler. and associate
			 * this handler with a key.
			 */
			for (Method m : cls.getDeclaredMethods()) {
				if (Modifier.isPublic(m.getModifiers())) {
					Annotation anno;
					if ((anno = m.getAnnotation(Get.class)) != null
							|| (anno = m.getAnnotation(Post.class)) != null
							|| (anno = m.getAnnotation(Put.class)) != null
							|| (anno = m.getAnnotation(Delete.class)) != null) {

						Handler newone = new Handler(module, m);

						String key = anno.annotationType().getSimpleName()
								.toUpperCase().concat(":/")
								.concat(normalize(appName)).concat("/")
								.concat(normalize(moduleName)).concat("/")
								.concat(normalize(m.getName()));
						Dispatcher.registerHandler(key, newone);

						log.info("Mapping " + key + " successfully.");

					}
				}
			}
		} catch (ClassNotFoundException | InstantiationException
				| IllegalAccessException e) {
			// typically such exceptions can be ignored. so do nothing about it.
		}

	}

	private void initVelocity(String config, String prefix, String suffix) {

		// set default values
		if (StringUtils.isEmpty(config))
			config = "velocity.properties";

		if (prefix == null)
			prefix = "";
		else
			prefix += "/";

		if (suffix == null)
			suffix = ".vm";
		else
			suffix = ".".concat(suffix);

		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		if (loader == null)
			loader = ClassLoader.getSystemClassLoader();

		Properties prop = new Properties();
		try {
			prop.load(loader.getResourceAsStream(config));
		} catch (IOException e) {
			log.error("Cannot load Velocity configuration file.", e);
		}

		PageWriter.init(prefix, suffix, prop);
	}

	private void initDataSource(String jdbcconf) {
		if (StringUtils.isEmpty(jdbcconf))
			jdbcconf = "jdbc.properties";

		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		if (loader == null)
			loader = ClassLoader.getSystemClassLoader();

		Properties prop = new Properties();
		try {
			prop.load(loader.getResourceAsStream(jdbcconf));
		} catch (IOException e) {
			log.error("Cannot load Velocity configuration file.", e);
		}

		Query.init(prop);
	}

	@Override
	public void contextInitialized(ServletContextEvent sce) {
		ServletContext sc = sce.getServletContext();
		initHandlers(sc.getInitParameter("packages"));
		initVelocity(sc.getInitParameter("vmconf"),
				sc.getInitParameter("vmprefix"),
				sc.getInitParameter("vmsuffix"));
		initDataSource(sc.getInitParameter("jdbcconf"));
	}

	@Override
	public void contextDestroyed(ServletContextEvent sce) {
		Query.destroy();
	}
	
	private static final Logger log = LogManager.getLogger(Listener.class);

}
